--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit b454757533dc2831e155ed89cdf18c5c581e49fe
Parents : 3f6ce06
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-29T14:38:44-05:00
refactor(meshchat): streamline transport identity handling and add extra config validation
Changes
2 files changed, 124 insertions(+), 14 deletions(-)
Diff
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index ab0b52e6..31234945 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -4386,22 +4386,13 @@ class ReticulumMeshChat:
},
status=422,
)
- transport_identity = data.get("transport_identity")
- if (
- transport_identity is None
- or str(transport_identity).strip() == ""
- ):
- return web.json_response(
- {
- "message": "Transport identity is required",
- },
- status=422,
- )
interface_details["remote"] = str(remote).strip()
interface_details["target_port"] = interface_target_port
- interface_details["transport_identity"] = str(
- transport_identity,
- ).strip()
+ InterfaceEditor.update_value(
+ interface_details,
+ data,
+ "transport_identity",
+ )
# handle I2P interface
if interface_type == "I2PInterface":
@@ -4802,6 +4793,42 @@ class ReticulumMeshChat:
interface_details["command"] = interface_command
interface_details["respawn_delay"] = interface_respawn_delay
+ _builtin_interface_types = frozenset(
+ {
+ "AutoInterface",
+ "TCPClientInterface",
+ "BackboneInterface",
+ "I2PInterface",
+ "TCPServerInterface",
+ "UDPInterface",
+ "RNodeInterface",
+ "RNodeIPInterface",
+ "RNodeMultiInterface",
+ "SerialInterface",
+ "KISSInterface",
+ "AX25KISSInterface",
+ "PipeInterface",
+ },
+ )
+ if interface_type not in _builtin_interface_types:
+ extra = data.get("extra_config")
+ if extra is None:
+ extra = {}
+ if not isinstance(extra, dict):
+ return web.json_response(
+ {
+ "message": "extra_config must be a JSON object",
+ },
+ status=422,
+ )
+ for key, value in extra.items():
+ if key in {"name", "type", "allow_overwriting_interface"}:
+ continue
+ if value is None or value == "":
+ interface_details.pop(key, None)
+ else:
+ interface_details[key] = value
+
# interface discovery options
for discovery_key in (
"discoverable",
@@ -13706,6 +13733,27 @@ class ReticulumMeshChat:
)
return
+ if lu.startswith("meshchatx://docs") or lu.startswith("meshchat://docs"):
+ from urllib.parse import parse_qsl, urlparse
+
+ parsed = urlparse(uri_raw)
+ q = dict(parse_qsl(parsed.query, keep_blank_values=True))
+ rel = (q.get("reticulum") or q.get("path") or "").strip()
+ payload: dict = {
+ "type": "lxm.ingest_uri.result",
+ "status": "success",
+ "message": "Opening documentation.",
+ "ingest_type": "docs_view",
+ }
+ if rel:
+ payload["docs_query"] = {"reticulum": rel}
+ AsyncUtils.run_async(
+ client.send_str(
+ json.dumps(payload),
+ ),
+ )
+ return
+
# Columba-style contact sharing URI:
# lxma://<destination_hash_hex>:<public_key_hex>
if uri.lower().startswith("lxma://"):
@@ -16697,6 +16745,12 @@ def main():
if args.no_crash_recovery:
recovery.disable()
+ planned_storage_dir = args.storage_dir or os.path.join("storage")
+ recovery.update_paths(
+ storage_dir=planned_storage_dir,
+ reticulum_config_dir=args.reticulum_config_dir,
+ )
+
# check if we want to test exception messages
if args.test_exception_message is not None:
raise Exception(args.test_exception_message)
diff --git a/tests/backend/test_interface_options.py b/tests/backend/test_interface_options.py
index e149d49d..a03a51fc 100644
--- a/tests/backend/test_interface_options.py
+++ b/tests/backend/test_interface_options.py
@@ -308,6 +308,62 @@ async def test_backbone_listener_mode_persists_options(temp_dir):
assert saved["prefer_ipv6"] is True
+@pytest.mark.asyncio
+async def test_backbone_connector_allows_empty_transport_identity(temp_dir):
+ config = ConfigDict({"reticulum": {}, "interfaces": {}})
+
+ free_port = _free_port("tcp")
+ async with make_app(temp_dir, config) as handler:
+ payload = {
+ "name": "BackboneOut",
+ "type": "BackboneInterface",
+ "target_host": "10.0.0.1",
+ "target_port": str(free_port),
+ }
+ response = await handler(make_request(payload))
+ body = json.loads(response.body)
+ assert response.status == 200, body
+ saved = config["interfaces"]["BackboneOut"]
+ assert saved["remote"] == "10.0.0.1"
+ assert str(saved["target_port"]) == str(free_port)
+ assert "transport_identity" not in saved
+
+
+@pytest.mark.asyncio
+async def test_external_interface_merges_extra_config(temp_dir):
+ config = ConfigDict({"reticulum": {}, "interfaces": {}})
+
+ async with make_app(temp_dir, config) as handler:
+ payload = {
+ "name": "WeaveTest",
+ "type": "WeaveInterface",
+ "extra_config": {"listen_ip": "127.0.0.1", "listen_port": 4242},
+ }
+ response = await handler(make_request(payload))
+ body = json.loads(response.body)
+ assert response.status == 200, body
+ saved = config["interfaces"]["WeaveTest"]
+ assert saved["type"] == "WeaveInterface"
+ assert saved["listen_ip"] == "127.0.0.1"
+ assert saved["listen_port"] == 4242
+
+
+@pytest.mark.asyncio
+async def test_external_interface_rejects_non_object_extra_config(temp_dir):
+ config = ConfigDict({"reticulum": {}, "interfaces": {}})
+
+ async with make_app(temp_dir, config) as handler:
+ payload = {
+ "name": "Bad",
+ "type": "FooInterface",
+ "extra_config": "not-an-object",
+ }
+ response = await handler(make_request(payload))
+ body = json.loads(response.body)
+ assert response.status == 422
+ assert "extra_config" in body["message"].lower()
+
+
@pytest.mark.asyncio
async def test_backbone_connector_mode_still_requires_remote(temp_dir):
config = ConfigDict({"reticulum": {}, "interfaces": {}})
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────